Skip to content

Conversation

@sterliakov
Copy link
Collaborator

@sterliakov sterliakov commented Oct 30, 2025

As discovered by @randolf-scholz in #20142, mypy treats value patterns in match statement in a completely wrong way.

From PEP 622:

Literal pattern uses equality with literal on the right hand side, so that in the above example number == 0 and then possibly number == 1, etc will be evaluated.

Existing tests for the feature are invalid: for example,

foo: object

match foo:
    case 1:
        reveal_type(foo)

must reveal object, and test testMatchValuePatternNarrows asserts the opposite. Here's a runtime example:

>>> class A:
...     def __eq__(self,o): return True
...     
>>> match A():
...     case 1:
...         print("eq")
...         
eq

I have updated the existing tests accordingly.

The idea is that value patterns are essentially equivalent to if foo == SomeValue checks, not isinstance checks modelled by conditional_types_wit_intersection.

The original implementation was introduced in #10191.

@github-actions

This comment has been minimized.

@sterliakov
Copy link
Collaborator Author

I'm frankly not sure what part of the core team to ping here - reviewers of the original PR, @JukkaL? @randolf-scholz please also take a look if you have time, IIRC some recent PRs of yours were related to pattern matching?

Primer finds nothing, apparently match isn't that popular in wild...

@sterliakov sterliakov marked this pull request as ready for review October 30, 2025 14:30
@randolf-scholz
Copy link
Contributor

Primer finds nothing, apparently match isn't that popular in wild...

Well, many popular projects still support 3.9, but match-case requires at least 3.10, so that's not surprising.

@randolf-scholz
Copy link
Contributor

randolf-scholz commented Oct 30, 2025

One thing that might be worth testing is if it interacts correctly with unions types and union patterns, e.g.

from typing import Literal

def test1(x: Literal[1,2,3]) -> None:
    match x:
        case 1:
            reveal_type(x)
        case other:
            reveal_type(x)

def test2(x: Literal[1,2,3]) -> None:
    match x:
        case 1:
            reveal_type(x)
        case 2:
            reveal_type(x)
        case 3:
            reveal_type(x)
        case other:
            assert_never(x)

def test3(x: Literal[1,2,3]) -> None:
    match x:
        case 1 | 3:
            reveal_type(x)
        case other:
            reveal_type(x)

and possibly the same with enum arguments / enum patterns.

@sterliakov
Copy link
Collaborator Author

I don't want to add any enum tests here, they won't be representative due to #19594 - I'd prefer to add match equivalents to tests there once this PR is merged. Unions are more relevant, thanks!

@github-actions

This comment has been minimized.

# like `if 1 < len(x) < 4: ...`
return reduce_conditional_maps(self.find_tuple_len_narrowing(node), use_meet=True)

def equality_type_narrowing_helper(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why you split this function, but it doesn't seem necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm? I split it because I want to reuse narrow_type_by_equality in another file, and there's no possibility of type(...) == ... checks as part of a checked pattern. Otherwise I'd have to create a fake node to call equality_type_narrowing_helper on it, and I'd rather avoid doing that

def visit_value_pattern(self, o: ValuePattern) -> PatternType:
current_type = self.type_context[-1]
typ = self.chk.expr_checker.accept(o.expr)
typ = coerce_to_literal(typ)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this coerce_to_literal still necessary now that we use narrow_type_by_equality rather than conditional_types_with_intersection?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It probably is; we are not in literal context, so coercion won't happen there for ==. Removing this will change the behavior significantly.

@github-actions
Copy link
Contributor

According to mypy_primer, this change doesn't affect type check results on a corpus of open source code. ✅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants